// Copyright 2011, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.adwords.axis.utils.v201109;

import com.google.api.ads.adwords.axis.v201109.cm.ReportDefinition;
import com.google.api.ads.adwords.lib.client.AdWordsSession;
import com.google.api.ads.adwords.lib.utils.ReportDownloadResponse;
import com.google.api.ads.adwords.lib.utils.ReportException;
import com.google.api.ads.common.lib.auth.OAuthAuthorizationHeaderProvider;
import com.google.api.ads.common.lib.exception.OAuthException;
import com.google.api.ads.common.lib.utils.StreamUtils;
import com.google.api.client.http.GenericUrl;
import com.google.common.annotations.VisibleForTesting;

import org.apache.axis.encoding.SerializationContext;
import org.apache.axis.encoding.Serializer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Reports are fetched synchronously like in the following code:
 *
 * <pre>
 * <code>new ReportDownloader(adWordsSession).downloadReport(reportDefinition);
 * </code>
 * </pre>
 *
 * The {@code adWordsSession} is used to authenticate the request against the
 * {@code reportDownloadUrl}.
 *
 * @author Adam Rogal
 * @author Kevin Winter
 */
public final class ReportDownloader {

  /** The URI of the download server. */
  private static final String DOWNLOAD_SERVER_URI = "/api/adwords/reportdownload";

  /** The version to append to url for Ad Hoc report downloads. */
  private static final String VERSION = "v201109";

  /** Regular expression used to detect errors messages in a response. */
  private static final String ERROR_MESSAGE_REGEX =
      "^.*!!![^|]*\\|\\|\\|([^|]*)\\|\\|\\|([^?]*)\\?\\?\\?.*$";

  /** Regular expression used to match attributes in xml tags. */
  private static final String REMOVE_ATTRIBUTES_REGEX =
      "( )?(xmlns|xsi):(\\w)+=\".*?\"|ns\\d:|<\\?xml.*?>";

  /** Regular expression used to remove self closing tags. */
  private static final String REMOVE_SELF_CLOSING_TAG = "<\\w+( )?/>";

  private static final String REPORT_BY_ID = "?__rd=%d";

  /** Regex group number that report exception message is stored in. */
  private static final int ERROR_MESSAGE_GROUP = 2;

  /** Length of the report head. */
  // Visible for testing.
  static final int REPORT_HEAD_LENGTH = 1024;

  /** HTTP Request Method */
  private static final String REQUEST_METHOD = "POST";

  private static final String SUCCESS = "SUCCESS";

  private final OAuthAuthorizationHeaderProvider oAuthAuthorizationHeaderProvider;

  private final AdWordsSession session;

  /**
   * Constructs a {@link ReportDownloader}.
   * @param session AdWordsSession to use to make report download requests.
   */
  public ReportDownloader(AdWordsSession session) {
    this.oAuthAuthorizationHeaderProvider = new OAuthAuthorizationHeaderProvider(REQUEST_METHOD);
    this.session = session;
  }

  /**
   * Checks for a report error in the head of the report and throws an
   * {@link ReportException} if found.
   *
   * @param reportHead the report head
   * @throws ReportException if the report contains an error
   */
  @VisibleForTesting
  static void checkForException(String reportHead) throws ReportException {
    Matcher matcher = Pattern.compile(ERROR_MESSAGE_REGEX).matcher(reportHead);
    if (matcher.matches()) {
      throw new ReportException(matcher.group(ERROR_MESSAGE_GROUP));
    }
  }

  /**
   * Downloads a report and returns a ReportDownloadResponse with the results.
   *
   * @param reportDefinitionId to download a report for.
   * @return {@link ReportDownloadResponse} When HTTP request completes. On
   *         success, the outputStream will be flushed and closed.
   * @throws ReportException If there is any issue making HTTP request with
   *         server.
   */
  public ReportDownloadResponse downloadReport(long reportDefinitionId) throws ReportException {
    try {
      String downloadUrl = generateReportUrl(reportDefinitionId);

      HttpURLConnection conn = getReportHttpUrlConnection(downloadUrl);

      conn.setRequestMethod("GET");
      conn.setDoOutput(true);

      int status = conn.getResponseCode();
      String responseMessage = SUCCESS;
      InputStream inputStream = null;
      if (status == HttpURLConnection.HTTP_OK) {
        inputStream = conn.getInputStream();
      } else {
        // Anything other than success means the body has an error message.
        responseMessage = new StreamUtils().readAll(conn.getErrorStream());
      }

      return new ReportDownloadResponse(status, responseMessage, inputStream);
    } catch (MalformedURLException e) {
      throw new ReportException("Created invalid report download URL.", e);
    } catch (IOException e) {
      throw new ReportException("Problem sending data to report download server.", e);
    }
  }

  /**
   * Downloads a report and returns a ReportDownloadResponse with the results.
   *
   * @param reportDefinition to download a report for.
   * @return {@link ReportDownloadResponse} When HTTP request completes. On
   *         success, the outputStream will be flushed and closed.
   * @throws ReportException If there is any issue making HTTP request with
   *         server.
   */
  public ReportDownloadResponse downloadReport(ReportDefinition reportDefinition)
      throws ReportException {
    try {
      String downloadUrl = generateReportUrl(null);

      HttpURLConnection conn = getReportHttpUrlConnection(downloadUrl);

      conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
      conn.setRequestMethod("POST");
      conn.setDoOutput(true);

      writeReportToStream(conn.getOutputStream(), reportDefinition);

      int status = conn.getResponseCode();
      String responseMessage = "SUCCESS";
      InputStream inputStream = null;
      if (status == HttpURLConnection.HTTP_OK) {
        inputStream = conn.getInputStream();
      } else {
        // Anything other than success means the body has an error message.
        responseMessage = new StreamUtils().readAll(conn.getErrorStream());
      }

      return new ReportDownloadResponse(status, responseMessage, inputStream);
    } catch (MalformedURLException e) {
      throw new ReportException("Created invalid report download URL.", e);
    } catch (IOException e) {
      throw new ReportException("Problem sending data to report download server.", e);
    }
  }

  /**
   * Serializes the report to XML and writes it form url-encoded to the provided
   * stream.
   *
   * @param outputStream to write to.
   * @param reportDefinition to write.
   * @throws IOException if there is a problem writing to the output stream.
   * @throws UnsupportedEncodingException if unicode is not supported.
   */
  @VisibleForTesting
  void writeReportToStream(OutputStream outputStream,
      ReportDefinition reportDefinition) throws UnsupportedEncodingException, IOException {
    String reportDefinitionXml = toXml(reportDefinition);
    OutputStreamWriter writer = new OutputStreamWriter(outputStream);
    writer.write("__rdxml=" + URLEncoder.encode(reportDefinitionXml, "UTF-8"));
    writer.close();
  }

  /**
   * Uses the built-in axis serializer to serialize the report and sanitize the
   * results.
   *
   * @param reportDefinition to serialize
   * @return Sanitized XML for the report download.
   * @throws IOException if axis has trouble serializing the ReportDefinition.
   */
  @VisibleForTesting
  String toXml(ReportDefinition reportDefinition) throws IOException {
    Serializer serializer =
        ReportDefinition.getSerializer("reportDefinition", ReportDefinition.class, ReportDefinition
            .getTypeDesc().getXmlType());
    StringWriter writer = new StringWriter();
    SerializationContext context = new SerializationContext(writer);
    serializer.serialize(ReportDefinition.getTypeDesc().getXmlType(), null, reportDefinition,
        context);
    return sanitize(writer.toString());
  }

  /**
   * Santizes the xml by removing all attributes, all self-closed tags as well
   * as renames the root element to <reportDefinition>
   *
   * @param string to sanitize.
   * @return Sanitized xml string suitable to send to the report download
   *         server.
   */
  @VisibleForTesting
  String sanitize(String string) {
    string = string.replaceAll(REMOVE_ATTRIBUTES_REGEX, "");
    string = string.replaceAll(REMOVE_SELF_CLOSING_TAG, "");
    string = string.replaceAll("ReportDefinition", "reportDefinition");
    return string;
  }

  /**
   * Creates the report download URL.
   *
   * @param reportDefinitionId if not null, added to URL.
   * @return url to download a report from.
   */
  @VisibleForTesting
  String generateReportUrl(Long reportDefinitionId) {
    String url = session.getEndpoint() + DOWNLOAD_SERVER_URI + '/' + VERSION;
    if (reportDefinitionId != null) {
      url += String.format(REPORT_BY_ID, reportDefinitionId);
    }
    return url;
  }

  /**
   * Gets the report HTTP URL connection given report URL and proper information
   * needed to authenticate the request.
   *
   * @param reportUrl the URL of the report response or download
   * @return the report HTTP URL connection
   * @throws MalformedURLException if the report URL could not be used
   * @throws IOException if there was a problem connecting to the URL
   * @throws ReportException if there were any other problems (such as errors
   *         with OAuth).
   */
  @VisibleForTesting
  HttpURLConnection getReportHttpUrlConnection(String reportUrl)
      throws MalformedURLException, IOException, ReportException {
    HttpURLConnection httpUrlConnection = (HttpURLConnection) new URL(reportUrl).openConnection();
    httpUrlConnection.setRequestMethod(REQUEST_METHOD);

    if (session.getClientLoginToken() != null) {
      httpUrlConnection.setRequestProperty("Authorization",
          "GoogleLogin auth=" + session.getClientLoginToken());
    } else if (session.getOAuthParameters() != null) {
      String authorizationHeader;
      try {
        authorizationHeader =
            oAuthAuthorizationHeaderProvider.getOAuthAuthorizationHeader(
                session.getOAuthParameters(), new GenericUrl(reportUrl));
        httpUrlConnection.setRequestProperty("Authorization", authorizationHeader);
      } catch (OAuthException e) {
        throw new ReportException("Could not set OAuth header", e);
      }
    } else {
      throw new IllegalArgumentException(
          "Either ClientLoginToken or OAuth header must be provided.");
    }

    httpUrlConnection.setRequestProperty("developerToken", session.getDeveloperToken());
    httpUrlConnection.setRequestProperty("clientCustomerId", session.getClientCustomerId());
    httpUrlConnection.setRequestProperty("returnMoneyInMicros",
        Boolean.toString(session.isReportMoneyInMicros()));

    // Required so that 301s to the final location don't trigger a redirect.
    httpUrlConnection.setInstanceFollowRedirects(false);
    return httpUrlConnection;
  }
}
